package org.algorithms.ricartagrawala;

import java.awt.EventQueue;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;

import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.UIManager;

import org.ActionConstants;
import org.ApplicationException;
import org.ConfigLoader;

public class ControllerImpl implements Controller {
	private static final int BUFFER_SIZE = 64;
	private ProcessCi[] processes;
	private DatagramSocket dataSocket;
	private RicartAgrawalaPanelImpl controlPanel;
	private Integer numberOfProcesses;
	private boolean mutexAcquiredByMe;
	private boolean trainMoving;
	private boolean iWantMutex;
	
	private int requestNumber;
	
	private int pid;
	// Request Queue
	private Queue<Integer> requestQ = new LinkedBlockingQueue<Integer>();
	
	// Acknowledgment Queue;
	private boolean[] ackQ = new boolean[3];
	
	public ControllerImpl(int pid, ProcessCi[] processes) {
		this.pid = pid;
		this.processes = processes;
		setNumberOfProcesses(new Integer(ConfigLoader.configBundle.getString("number.of.processes")));
	}
	
	public static void main(String[] args) {
		try {
			UIManager.setLookAndFeel("javax.swing.plaf.metal.MetalLookAndFeel");
			JFrame.setDefaultLookAndFeelDecorated(true);
			JDialog.setDefaultLookAndFeelDecorated(true);
		} catch (Throwable e) {	}
		
		EventQueue.invokeLater(new Runnable() {
			public void run() {
				try {
					RicartAgrawalaPanelImpl frame = new RicartAgrawalaPanelImpl();
					frame.setVisible(true);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		});
	}
	
	@Override
	public void initialSetup() throws ApplicationException {
		try {
			createSocket();
		} catch (SocketException e) {
			throw new ApplicationException(e.getMessage());
		}
		doRicartAgrawalaAlgorithm();
	}

	private int getPortForProcess(int processNumber) {
		String strCiPort = "c" + processNumber + ".port";
		return new Integer(ConfigLoader.configBundle.getString(strCiPort)).intValue();
	}
	
	private void setPortForProcesses() {
		for (int i=0; i < processes.length; i++) {
			processes[i].setPort( getPortForProcess( i+1 ) );
		}
	}
	
	private void createSocket() throws SocketException {
		setPortForProcesses();
		Integer portNumber = getPortForProcess(pid);
		dataSocket = new DatagramSocket(portNumber, processes[pid-1].getIpAddress());
//		getControlPanel().logMessage("C" + pid +" listening in Port:  " + dataSocket.getLocalPort(), false);
	}
	
	@Override
	public void sendActionToAll(int actionNumber) {
		ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(BUFFER_SIZE);
		DataOutputStream dataOutputStream = new DataOutputStream(byteArrayOutputStream);
		
		try {
			dataOutputStream.writeInt(pid);
			dataOutputStream.writeInt(actionNumber);
		} catch (IOException e) { }
		
		final byte[] outputBuffer = byteArrayOutputStream.toByteArray();

		DatagramPacket dataPacket;
		for (int i=0; i < processes.length; i++) {
			ProcessCi ci = processes[i];
			
			dataPacket = new DatagramPacket(
					outputBuffer, outputBuffer.length,
					ci.getIpAddress(), ci.getPort());
			try {
				System.out.println("Sending action " + actionNumber + " to p" + i);
				dataSocket.send(dataPacket);
			} catch (IOException e) { }
		}
	}
	
	public void sendActionToProcess(int other_pid, int actionNumber) {
		ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(BUFFER_SIZE);
		DataOutputStream dataOutputStream = new DataOutputStream(byteArrayOutputStream);
		
		try {
			dataOutputStream.writeInt(pid);
			dataOutputStream.writeInt(actionNumber);
		} catch (IOException e) {
			// Log Failure
		}
		
		final byte[] outputBuffer = byteArrayOutputStream.toByteArray();

		DatagramPacket dataPacket;
		
		ProcessCi ci = processes[other_pid-1];
		
		dataPacket = new DatagramPacket(
				outputBuffer, outputBuffer.length,
				ci.getIpAddress(), ci.getPort());
		try {
			dataSocket.send(dataPacket);
		} catch (IOException e) { }
	}
	
	private void sendActionToTrain(Integer actionNumber) {
		ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(32);
		DataOutputStream dataOutputStream = new DataOutputStream(byteArrayOutputStream);
		
		try {
			dataOutputStream.writeInt(actionNumber);
		} catch (IOException e) { }
		
		final byte[] outputBuffer = byteArrayOutputStream.toByteArray();
		int port = getDataSocket().getLocalPort() + getNumberOfProcesses();
		DatagramPacket dataPacket = new DatagramPacket(
				outputBuffer, 
				outputBuffer.length,
				processes[pid-1].getIpTrain(), 
				port);
		try {
			dataSocket.send(dataPacket);
		} catch (IOException e) { }
	}
	
	public void doRicartAgrawalaAlgorithm() {
		final byte[] inputBuffer = new byte[BUFFER_SIZE];
		final ByteArrayInputStream arrayInputStream = new ByteArrayInputStream(inputBuffer);
		final DataInputStream dataInputStream = new DataInputStream(arrayInputStream);
		final DatagramPacket dataPacket = new DatagramPacket(inputBuffer, inputBuffer.length);
		
		Runnable runnable1 = new Runnable() {
			public void run() {
				Runnable runnable2;
				while (true) {
					try {
						System.out.println("Pid: " + pid + " waiting to receive signal...");
						dataSocket.receive(dataPacket);
					} catch (Throwable e) {
						e.printStackTrace();
						continue;
					}
					
					arrayInputStream.reset();
					try {
						final int other_pid = dataInputStream.readInt();
						final int actionNumber = dataInputStream.readInt();
						final int clock = dataInputStream.readInt();
		
						System.out.println("Signal received from pid: " + pid + " action: " + actionNumber);
						
						runnable2 = new Runnable() {
							public void run() {
								handleReceivedAction(other_pid, actionNumber, clock);
							}
						};
						new Thread(runnable2).start();
						
					} catch (IOException e) {
						e.printStackTrace();
					}
				}
			}
		};
		new Thread(runnable1).start();
	}
	
	private void handleReceivedAction(int other_pid, int actionNumber, int clock) {
		switch (actionNumber) {
		case ActionConstants.SOMEONE_WANTS_MUTEX :
			boolean isMyPriority = (this.pid < other_pid); 
			if (isMutexAcquiredByMe() ||
			   (iWantMutex() && isMyPriority)) {
				requestQ.add(new Integer(other_pid));
			} else {
				getControlPanel().getBtnInitControl().setEnabled(false);
				sendActionToProcess(other_pid, ActionConstants.ACKNOWLEDGMENT);
			}
			break;

		case ActionConstants.ACKNOWLEDGMENT :
			ackQ[other_pid-1] = true;
			if (ackQ[0] && ackQ[1] && ackQ[2]) {
				// mutual exclusion acquired
				// adjusts the panel
				getControlPanel().getBtnInitControl().setEnabled(false);
				getControlPanel().getBtnReleaseControl().setEnabled(true);
				if (isTrainMoving()) {
					getControlPanel().getBtnPauseMove().setEnabled(true);
					getControlPanel().getBtnReinitMove().setEnabled(false);
				} else {
					getControlPanel().getBtnReinitMove().setEnabled(true);
					getControlPanel().getBtnPauseMove().setEnabled(false);
				} 
				ackQ[0] = ackQ[1] = ackQ[2] = false;
			}
			break;
			
		case ActionConstants.INIT_MOVEMENT:
			setTrainMoving(true);
			break;
		case ActionConstants.PAUSE_MOVEMENT:
			setTrainMoving(false);
			break;
		case ActionConstants.ENABLE_INIT_CONTROL :
			getControlPanel().getBtnInitControl().setEnabled(true);
			break;
		}
	}
	
	@Override
	public void handleControlPanelAction(Integer actionNumber) {
		switch (actionNumber) {
		case ActionConstants.INIT_CONTROL :
			setiWantMutex(true);
			sendActionToAll(ActionConstants.SOMEONE_WANTS_MUTEX);
			break;
			
		case ActionConstants.RELEASE_CONTROL :
			setiWantMutex(false);
			setMutexAcquiredByMe(false);
			
			// if there are processes in the queue, then someone will use CS
			int trainState = (isTrainMoving()) ? ActionConstants.INIT_MOVEMENT : ActionConstants.PAUSE_MOVEMENT;
			if (!requestQ.isEmpty()) {
				// after using CS send acknowledgment to everyone in the queue
				for (Integer queuedPid : requestQ) {
					sendActionToProcess(queuedPid, trainState);
					sendActionToProcess(queuedPid, ActionConstants.ACKNOWLEDGMENT);
				}
				getControlPanel().getBtnInitControl().setEnabled(false);
				// after having done the job clear the queue
				requestQ.clear();
			} else {
				sendActionToAll(trainState);
				sendActionToAll(ActionConstants.ENABLE_INIT_CONTROL);
			}
			getControlPanel().getBtnPauseMove().setEnabled(false);
			getControlPanel().getBtnReinitMove().setEnabled(false);
			getControlPanel().getBtnReleaseControl().setEnabled(false);
			break;
			
		case ActionConstants.INIT_MOVEMENT :
			sendActionToTrain(ActionConstants.INIT_MOVEMENT);
			setTrainMoving(true);
			getControlPanel().getBtnPauseMove().setEnabled(true);
			getControlPanel().getBtnReinitMove().setEnabled(false);
			break;
			
		case ActionConstants.PAUSE_MOVEMENT :
			sendActionToTrain(ActionConstants.PAUSE_MOVEMENT);
			setTrainMoving(false);
			getControlPanel().getBtnPauseMove().setEnabled(false);
			getControlPanel().getBtnReinitMove().setEnabled(true);
			break;
		}
	}
	
	/*
	 * GETTERS AND SETTERS 
	 */
	
	@Override
	public ProcessCi[] getProcesses() {
		return processes;
	}
	
	@Override
	public void setProcesses(ProcessCi[] processes) {
		this.processes = processes;
	}

	public DatagramSocket getDataSocket() {
		return dataSocket;
	}

	public void setDataSocket(DatagramSocket dataSocket) {
		this.dataSocket = dataSocket;
	}
	
	@Override
	public RicartAgrawalaPanelImpl getControlPanel() {
		return controlPanel;
	}
	
	@Override
	public void setControlPanel(RicartAgrawalaPanelImpl controlPanel) {
		this.controlPanel = controlPanel;
	}

	@Override
	public Integer getNumberOfProcesses() {
		return numberOfProcesses;
	}
	
	@Override
	public void setNumberOfProcesses(Integer numberOfProcesses) {
		this.numberOfProcesses = numberOfProcesses;
	}

	public boolean isMutexAcquiredByMe() {
		return mutexAcquiredByMe;
	}

	public void setMutexAcquiredByMe(boolean mutexAcquiredByMe) {
		this.mutexAcquiredByMe = mutexAcquiredByMe;
	}

	public boolean isTrainMoving() {
		return trainMoving;
	}

	public void setTrainMoving(boolean trainMoving) {
		this.trainMoving = trainMoving;
	}

	public boolean iWantMutex() {
		return iWantMutex;
	}

	public void setiWantMutex(boolean iWantMutex) {
		this.iWantMutex = iWantMutex;
	}

	public int getRequestNumber() {
		return requestNumber;
	}

	public void setRequestNumber(int requestNumber) {
		this.requestNumber = requestNumber;
	}
}